在實作 RAG 檢索時,我發現了一個非常隱蔽卻致命的「時間陷阱」:
如果我在三天前跟 Agent 說:「我下週二有資工系期末考,最近壓力很大。」
到了今天,當我跟她聊起:「今天考試終於考完了,好輕鬆喔!」
傳統的向量搜尋(Similarity Search)只看「文字語意相近」,結果 ChromaDB 依然會把三天前那條「下週二要考試」的記憶撈出來。這會導致 Agent 冒出一句:「你不是下週二才要考試嗎?現在放鬆什麼?」,直接產生時間錯亂的崩壞感!
為了解決這個問題,今天我們來實作 Time-Aware(時間感知)檢索機制,讓陪伴 Agent 的外掛大腦不只要懂得「語意」,還要懂得「時間的流逝」!
要讓 Agent 擁有時間感,關鍵在於兩點:
Metadata 一併存入 ChromaDB。rag_memory.py 支援時間戳寫入打開 rag_memory.py,我們為使用者記憶加上 UTC 時間戳記,並新增支援讀取 Metadata 的查詢函式:
from datetime import datetime, timezone
# --- 軌道 B:使用者記憶相關 API ---
def add_user_memory(memory_id: str, text: str, user_id: str = "default_user"):
"""記錄使用者的偏好、說過的小事(自動附帶 UTC 時間戳)"""
# 存 UTC 而不是本地時間:時區資訊帶在字串裡,之後換機器或部署到雲端都不會錯亂
now_iso = datetime.now(timezone.utc).isoformat()
user_memory_collection.upsert(
documents=[text],
metadatas=[{
"user_id": user_id,
"created_at": now_iso,
}],
ids=[memory_id]
)
def query_user_memory_with_time(query_text: str, n_results: int = 2) -> list[dict]:
"""搜尋使用者記憶,同時帶回「文本」與「建立時間」。
純語意搜尋只看文字像不像,撈不出時間感,三天前說「下週二要考試」,
今天說「終於考完了」還是會把那筆撈出來,莉莉就會冒出
「你不是下週二才要考嗎」這種時間錯亂的話。
把 created_at 一起帶回去,讓模型自己判斷那是多久以前的事。
Returns:
list[dict]: 每筆包含 {'text': str, 'created_at': str}
created_at 是 UTC ISO 字串;
加時間戳之前寫入的舊資料沒有這個欄位,會拿到空字串
"""
results = user_memory_collection.query(
query_embeddings=_query_ef([query_text]),
n_results=n_results,
include=["documents", "metadatas"] # 要拿 created_at 就得明確要求 metadatas
)
if not results['documents'] or not results['documents'][0]:
return []
return [
{"text": doc, "created_at": (meta or {}).get("created_at", "")}
for doc, meta in zip(results['documents'][0], results['metadatas'][0])
]
rag_builder.py(動態計算時間差距與時間錨點)有了上面的 rag_memory.py 之後,我寫一個組裝器,負責計算「這條記憶是多久前的」:
from datetime import datetime, timezone
import rag_memory
def _calculate_time_ago(iso_str: str) -> str:
"""計算記憶發生的時間距今多久"""
if not iso_str:
return "過去"
try:
past_time = datetime.fromisoformat(iso_str)
now = datetime.now(timezone.utc)
diff = now - past_time
if diff.total_seconds() < 0:
return "剛剛"
days = diff.days
hours = diff.seconds // 3600
minutes = (diff.seconds % 3600) // 60
if days > 0:
return f"{days} 天前"
elif hours > 0:
return f"{hours} 小時前"
elif minutes > 0:
return f"{minutes} 分鐘前"
else:
return "剛剛"
except Exception as e:
# 不要靜默吞掉:最常見的是 tz-aware 與 naive 時間相減,
# 只回傳「不久前」的話,你永遠不會知道時間戳其實壞了
print(f"[rag_builder] 時間解析失敗:{iso_str!r} — {type(e).__name__}: {e}")
return "不久前"
def build_rag_context(user_input: str) -> str:
"""根據使用者輸入,動態組裝包含時間感知 (Time-Aware) 的 Context Prompt"""
lores = rag_memory.query_character_lore(user_input, n_results=1)
memories = rag_memory.query_user_memory_with_time(user_input, n_results=2)
context_parts = []
if lores:
lore_text = "\n".join([f"- {lore}" for lore in lores])
context_parts.append(f"【檢索到的角色設定 (Lore)】:\n{lore_text}")
if memories:
memory_lines = []
for m in memories:
time_ago = _calculate_time_ago(m["created_at"])
memory_lines.append(f"- [{time_ago}] {m['text']}")
memory_text = "\n".join(memory_lines)
context_parts.append(f"【關於使用者的歷史記憶(包含時間註記)】:\n{memory_text}")
if not context_parts:
return ""
full_context = "\n\n".join(context_parts)
# 告訴模型「現在」是什麼時候。
# 模型只知道訓練資料的年代,實測問它今天幾號,它會答成兩年前的日期。
# 但使用者說的「下週二」是絕對時間,沒有今天的錨點就無從推算過了沒。
# 用本地時間而非 UTC,因為使用者講的「下週二」是本地概念。
now_local = datetime.now().astimezone()
weekday_zh = "一二三四五六日"[now_local.weekday()]
now_text = f"{now_local.strftime('%Y-%m-%d %H:%M')}(星期{weekday_zh})"
return (
f"--- [RAG 檢索輔助記憶] ---\n"
f"【現在時間】{now_text}\n\n"
f"{full_context}\n"
f"(注意事項:請根據記憶發生的時間差距與邏輯來回應。若記憶為數天前的預定事項,"
f"請自行推算是否已發生或完成,維持對話自然度。)"
)
【RAG 實戰成果驗證】
當我刻意詢問:「我下週二的考試是不是快到了?」
背景運作:RAG 檢索出 [3 天前] 使用者下周二要參加資工系的期末考,最近壓力很大。,並且 prompt 中包含了目前的真實時間。
莉莉回應:
「哈?!你現在是在說夢話嗎?今天都星期四了,下週二不就只剩幾天了嗎?你這傢伙到底有沒有在緊張啊?要是到時候考砸了哭著臉回來,我絕對不會安慰你的喔!聽懂了就快點給我看書去,笨蛋!」

當我刻意詢問:「我下週二的考試是不是快到了?」
系統展現了非常驚艷的檢索與推理能力:
GeminiEmbeddingFunction 指定 RETRIEVAL_DOCUMENT 與 RETRIEVAL_QUERY 雙向 Task Type,語意比對準確度大幅提升。created_at(UTC ISO 格式),確保時區跨平台部署時不會發生混亂。